<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html><head>

    
        <title>
realm3: Drag and Drop Trees With CakePHP        </title>

        <!-- Meta Tags -->
		<link rel="alternate" href="http://feeds.feedburner.com/r3articles" title="r3: Articles" type="application/rss+xml">
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <meta name="robots" content="index, follow">
        <meta name="description" content="CakePHP's TreeBehavior is quite useful, but wouldn't it be nice if we could save parent/child/sibling relationships in a nice drag &amp; drop UI?">
        <meta name="keywords" content="realm3,security,web,application,software,development,architecture,freelance,consultant,consulting,,business,entrepreneur,new york,nyc,nj,new jersey,long island">
        <meta name="author" content="Brian Dailey">
		<link rel="stylesheet" type="text/css" href="drag_and_drop_trees_with_cakephp_files/r3.css">		<script src="drag_and_drop_trees_with_cakephp_files/r3.js" type="text/javascript">
		</script>
    </head><body>
		<div id="main">
			<div style="text-align: center; margin-top: 15px; margin-bottom: 15px;"><a href="http://realm3.com/"><img src="drag_and_drop_trees_with_cakephp_files/logo.gif" alt="realm3: web applications" border="0"></a></div>
			<div id="menu">
				<ul>
					<li><a href="http://realm3.com/">Home</a></li>
					<li><a class="who" href="http://realm3.com/pages/freelance_developer_new_york">Who</a></li>
					<li><a class="articles" href="http://realm3.com/posts/">Articles</a></li>
				</ul>
			</div>
			<br>
<div class="post">
<h2 class="greenstripe"><img src="drag_and_drop_trees_with_cakephp_files/articles_icon.gif" align="right"><a href="http://realm3.com/posts">&lt;List Articles&gt;</a>Drag and Drop Trees With CakePHP</h2>
<div class="date">Originally posted June 26, 2008</div>
<img src="drag_and_drop_trees_with_cakephp_files/children.png" style="border: 1px solid rgb(0, 0, 0); margin-left: 10px; margin-bottom: 10px;" alt="A screenshot of the parent/child relationship in a UL list." align="right">
<p>The "TreeBehavior" built into CakePHP has a number of useful
applications beyond the Auth/ACL uses that most developers use it for.
As such, it's good to have a way to manipulate the tree relationships
in an intuitive, user-friendly way. The goals in this particular
project were to create a drag &amp; drop UL/LI list that would retain
not only the parent relationships, but also the sibling relationships.</p>
<p>In this example, let's create an organizational chart. This is
something you'd see that demonstrates the relationships amongst
employees at an organization - something that could potentially change
over time.</p>
<h3>Prepping The Database</h3>
<p>First, obviously, we need a table. Here's what I'm using in this example:</p>
<pre class="code">CREATE TABLE `positions` (
  `id` mediumint(9) NOT NULL auto_increment,
  `position_name` varchar(100) default NULL,
  `parent_id` mediumint(9) NOT NULL default '0',
  `lft` mediumint(9) NOT NULL default '0',
  `rght` mediumint(9) NOT NULL default '0',
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=75 DEFAULT CHARSET=utf8;
</pre>
<p>Go ahead and create this table first, then go to your CakePHP application directory and use the <a href="http://manual.cakephp.org/view/113/code-generation-with-bake">bake shell to create the model</a>.</p>

<pre># bake

Welcome to CakePHP v1.2.0.7125 RC1 Console
---------------------------------------------------------------
App : app
Path: /[path/to]/demo/app
---------------------------------------------------------------
Interactive Bake Shell
---------------------------------------------------------------
[D]atabase Configuration
[M]odel
[V]iew
[C]ontroller
[P]roject
[Q]uit
What would you like to Bake? (D/M/V/C/P/Q) 
&gt; <b>M</b>
---------------------------------------------------------------
Bake Model
Path: /[path/to]/demo/app/models/
---------------------------------------------------------------
Possible Models based on your current database:
1. Position
Enter a number from the list above, type in the name of another model, or 'q' to exit  
[q] &gt; <b>1</b>
Would you like to supply validation criteria for the fields in your model? (y/n) 
[y] &gt; <b>n</b>
Would you like to define model associations (hasMany, hasOne, belongsTo, etc.)? (y/n) 
[y] &gt; <b>n</b>

---------------------------------------------------------------
The following Model will be created:
---------------------------------------------------------------
Name:       Position
Associations:
---------------------------------------------------------------
Look okay? (y/n) 
[y] &gt; <b>y</b>

Baking model class for Position...

Creating file /[path/to]/demo/app/models/position.php
Wrote /[path/to]/demo/app/models/position.php
Cake test suite not installed.  Do you want to bake unit test files anyway? (y/n) 
[y] &gt; <b>y</b>

You can download the Cake test suite from http://cakeforge.org/projects/testsuite/

Baking test fixture for Position...

Creating file /[path/to]/demo/app/tests/fixtures/position_fixture.php
Wrote /[path/to]/demo/app/tests/fixtures/position_fixture.php

Baking unit test for Position...

Creating file /[path/to]/demo/app/tests/cases/models/position.test.php
Wrote /[path/to]/demo/app/tests/cases/models/position.test.php
---------------------------------------------------------------
</pre>
<p>Now that we've got our model built, we need to identify it as a "Tree." We'll do this using the <a href="http://manual.cakephp.org/view/90/using-behaviors">Behavior</a> mechanism in CakePHP. All we're adding is the following line inside our model.</p>
<pre class="code">	var $actsAs = array('Tree');
</pre>
<p>You can go ahead and use the bake console to create the controller
and views for this model. Go ahead and create two records via the
generated CRUD pages - just don't give any values for parent/lft/right.
Both records will be top-level nodes in the tree.</p>

<h3>Creating The View</h3>
<p>The next step is creating an HTML ul/li list of the tree so we can see what it looks like. There is already a <a href="http://api.cakephp.org/1.2/class_tree_behavior.html#81475c3241066c67f987bcaa547317e9">TreeBehavior::generatetreelist()</a>
function for creating a semi-useful array with each node padded for
each parent. This is only marginally useful, as it doesn't look very
good in HTML output at all. Instead, we'll make use of this function to
create a ul/li list showing the relationships. For this, we'll use a
Helper. Inside your app/views/helpers/ directory you can use the
following code. In this example I'm calling it TreeViewHelper, which
would be inside a file called tree_view.php.</p>
<p>(Note: Using TreeBehavior::generatetreelist() probably isn't the <i>best</i>
way to do this - the best way would be using code similar to output the
ul/li list directly, but for several reasons I chose to do it this way.
Your mileage may vary.)</p>
<div style="overflow: auto;">
<pre class="code">&lt;?php
class TreeViewHelper extends Helper {
    
    function createListTree($tree) {
        $out = '';
    
        $depth = 0;
        $prev_depth = 0;
        $count = 0;
        foreach ($tree as $id =&gt; $node) {
            $depth = strrpos($node, '_');
            if ($depth === false) {
                $depth = 0;
                $clean_node = $node;
            } else {
                $depth = $depth + 1;
                $clean_node = substr($node, strrpos($node, '_')+1);
            }

            if ($depth &gt; $prev_depth) {
                $out .= "\n&lt;ul&gt;\n";
            } else if ($depth &lt; $prev_depth) {
                for ($i = 0; $i &lt; ($prev_depth-$depth); $i++) {
                    $out .= "&lt;/li&gt;&lt;/ul&gt;\n";
                }
            } else if ($count&gt;0) {
                $out .= "\n&lt;/li&gt;\n";
            }

            $out .= '&lt;li id="node_' . $id . '" noDelete="true" noRename="true"&gt;&lt;a class="drag" href="/positions/view/' . $id . '/' . $id . '/"&gt;' . $clean_node . '&lt;/a&gt;';
            $out .= "\n";

            $prev_depth = $depth;
            $count++;
        }
        for ($i = 0; $i &lt; ($depth); $i++) {
            $out .= "&lt;/li&gt;&lt;/ul&gt;\n";
        }
        if (!empty($tree)) {
            $out .= '&lt;/li&gt;';
        }

        return $out;
    }
}

</pre></div>
<p>Inside our controller, we'll need to add the TreeView helper.</p>
<pre class="code">	var $helpers = array('Html', 'Form', 'TreeView');
</pre>
<p>Create a controller function for our view (I'm using 'treeview') and then the view itself (/app/views/positions/treeview.ctp).</p>
<p>Here's the beginnings of our function:</p>
<pre class="code">    function treeview() {
        $this-&gt;set('positions', $this-&gt;Position-&gt;generateTreeList());
    }
</pre>
<p>And the view:</p>

<pre>    &lt;ul id="role_tree"&gt;
    &lt;?php
        echo $treeView-&gt;createListTree($positions);
    ?&gt;  
    &lt;/ul&gt;   
</pre>
<p>If you load the page, viola! A tree! The only problem is that you're
probably seeing ID's, not the 'Position Name.' This is easily solved by
adding this to your model:</p>
<pre>    var $displayField = 'position_name';
</pre>

<p>Refresh the page, and now you should be seeing your two sibling records in a ul list.</p>
<h3>Drag &amp; Drop</h3>
<p>Now for the drag and drop. Instead of reinventing the wheel on this one we're going to use some code that <a href="http://www.dhtmlgoodies.com/index.html?whichScript=drag-drop-folder-tree">DHTML Goodies</a> has made available for use. It's not the cleanest of all possible code, but it works for our purposes.</p>
<p>I downloaded the zip file and unzipped it in
/app/webroot/js/dragdrop/. It contains all the images, etc, necessary
for this project. If you want to make changes to it, you'll need some
familiarity with Javascript and CSS, which I assume you have.</p>
<p>Once it's unzipped, we'll include it in our view. I've already added
the necessary 'node' tags to the HTML output, so all you'll need to do
is update your view so it looks like this:</p>
<div style="overflow: auto;">
<pre class="code">&lt;script type="text/javascript" src="/js/dragdrop/js/ajax.js"&gt;&lt;/script&gt;

&lt;script type="text/javascript" src="/js/dragdrop/js/context-menu.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="/js/dragdrop/js/drag-drop-folder-tree.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" type="text/css" href="/js/dragdrop/css/drag-drop-folder-tree.css" /&gt;
&lt;link rel="stylesheet" type="text/css" href="/js/dragdrop/css/context-menu.css" /&gt;
    &lt;a href="javascript:saveNodes();"&gt;Save Changes&lt;/a&gt;

    &lt;ul id="tree" class="dhtmlgoodies_tree"&gt;

    &lt;?php
        echo $treeView-&gt;createListTree($positions);
    ?&gt;
    &lt;/ul&gt;    
&lt;script language="javascript"&gt;
    window.onload=function() {
        treeObj = new JSDragDropTree();
        treeObj.setTreeId('tree');
        treeObj.setMaximumDepth(15);
        treeObj.setMessageMaximumDepthReached('Maximum depth reached');
        treeObj.setImageFolder('/js/dragdrop/images/');
        treeObj.initTree();
        treeObj.expandAll();
    };
    function saveNodes() {
        save_string = treeObj.getNodeOrders();
        location.href='/positions/save/?save_string=' + save_string;
    }
&lt;/script&gt;
</pre></div>
<p>If you reload the page, you should now be able to drag &amp; drop
nodes. The next step is to save the changes. I've already added the
link and the javascript function to the view. Note that for the
purposes of this particular demonstration, I'm not using the AJAX
method that is available. It's pretty easy to change that, if you wish
to.</p>

<p>We're going to add a new function to our controller.</p>
<div style="overflow: auto;">
<pre class="code">    function save() {
        // First get the tree in it's original format.
        $tree = $this-&gt;Position-&gt;generateTreeList();

        // Save string looks like ID-PARENTID,ID-PARENTID,....
        $save_string = $this-&gt;params['url']['save_string'];

        $associations = explode(',', $save_string);
        foreach ($associations as $relationship) {
            $ids = explode('-', $relationship);
            $saved_ids[] = $ids[0];

            $this_node_id = $ids[0];
            $this_node_parent_id = $ids[1];

            $this-&gt;Position-&gt;recursive = -1;
            $position = $this-&gt;Position-&gt;find('first', array('conditions' =&gt; array(
                    'Position.id' =&gt; $this_node_id,
                )   
            ));
            $this-&gt;Position-&gt;set($position);
            if (isset($position['Position']['id']) &amp;&amp; $position['Position']['parent_id'] !== $this_node_parent_id) {   
                $ret = $this-&gt;Position-&gt;setParent($this_node_parent_id);
                if ($ret === false) {
                    $this-&gt;Session-&gt;setFlash('Failed to save the organizational chart.');
                    $this-&gt;redirect(array('action' =&gt; 'treeview'));
                    break;
                }
            }
        }

        if (array_keys($tree) != $saved_ids) {
            // Need to reorder based on sibling heirarchy changes!
            $count = count($saved_ids) - 1;
            for ($i = $count; $i &gt;= 0; $i--) {
                $ret = $this-&gt;Position-&gt;moveUp($saved_ids[$i], true);
                // Returns false when we move an already-at-top to the top, so ignore falses.
            }
        }

        $this-&gt;Session-&gt;setFlash('Saved changes.');
        $this-&gt;redirect(array('action' =&gt; 'treeview'), null, true);
    }

</pre></div>
<p>Once you've saved this in your controller, you should be able to
click the "Save Changes" link on the view and it will work properly. To
create more nodes, simply add new top-level records; after creating
them you'll be able to drag them anywhere in the hierarchy.</p>
<h3>Questions, Comments?</h3>
<p> I'm not sure how this would work if you had two people saving changes at the same time. I'm fairly sure it wouldn't be pretty.</p>
<p>Other than that, it seems to work pretty well. I haven't noticed a
significant slow-down when there are hundreds of records that must be
re-arranged (you may notice that saving the changes can result in a lot
of database updates).</p>
<p>If you have any comments, criticisms, etc, please feel free to share
them. Hopefully this will be helpful to others who were trying to
accomplish similar goals as I was in working on this project.</p>
</div>
<div class="related">
	<h3>Related Comments<a name="addcomments">&nbsp;</a></h3>
		<div class="comment">
		<div class="date">
			Thu, 31 Jul 2008 04:14:07 -0400		</div>
		<div class="name">
						quangtruong1985					</div>
		<p>Great job man! Keep working like that!<br>
Can u create a function emitate WIndows Explorer tree view? I hope to see that soon!<br>
^_^ sorry for my noob English</p>
	</div>
	<h3>Add New Comment</h3>
	<div class="comment">
		<form name="Comment" action="/posts/view/20#addcomments" method="post"> 
			<div class="input"><label for="CommentName">Name</label><input name="data[Comment][name]" class="required" value="" id="CommentName" type="text"></div>			<div class="input"><label for="CommentEmail">Email (Not Displayed)</label><input name="data[Comment][email]" class="required" value="" id="CommentEmail" type="text"></div>			<div class="input"><label for="CommentWebsite">Website</label><input name="data[Comment][website]" value="" id="CommentWebsite" type="text"></div>			<div class="input"><label for="CommentAntispam">Please verify you are human. What is 400 minus 1?</label><input name="data[Comment][antispam]" value="" id="CommentAntispam" type="text"></div>			<div class="input"><label for="CommentBody">Comment</label><textarea name="data[Comment][body]" cols="30" rows="6" id="CommentBody"></textarea></div>			<div class="submit"><input value="Add Comment" type="submit"></div>			<br>
		</form>
	</div>
</div>
        <br style="clear: both;">
    </div>
	<script src="drag_and_drop_trees_with_cakephp_files/urchin.js" type="text/javascript">
	</script>
	<script type="text/javascript">
		_uacct = "UA-121463-1";
		urchinTracker();
	</script>
    </body></html>